/*------------------------------------------------------------------------------
 * Copyright 2012 NCR Corporation
 *------------------------------------------------------------------------------
 * All revision information is updated automatically from source code control
 * change records - please do not manually edit.
 *------------------------------------------------------------------------------
 *
 *  $LastChangedRevision::                                          $
 *  $LastChangedBy::                                                $
 *  $LastChangedDate::                                              $
 */

package com.ncr.nep.dashboardcore.ui.client;

import static com.ncr.nep.dashboardcore.ui.client.DOMWrapper.$;

import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.Node;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.ui.HTMLPanel;
import com.google.gwt.user.client.ui.IsWidget;
import com.google.gwt.user.client.ui.Widget;
import com.ncr.nep.dashboardcore.utils.client.StringUtil;

/**
 * Panel that can be used to create a fluid grid layout that maintains column
 * dimensions as the browser is re-sized.
 *
 * <p>
 *    The grid itself is comprised of 0 or more rows (divs) that occupy 100%
 *    of the available parent's width.  Each row can contain 0 or more columns.
 * </p>
 *
 * <p><strong>Column sizing</strong></p>
 * <p>
 *     In order to achieve a fluid sizing across browsers (including mobile),
 *     the FluidGrid divides the available width into 12 equally sized sections
 *     with gutters.  To set the size of a column, you specify the number of
 *     horizontal slices it should occupy.  For instance, you could specify the
 *     size of a column to be 4, meaning that the column will occupy 4 slices
 *     worth of space.
 * </p>
 *
 * @author rb250096
 * @version $Revision$
 */
public class FluidGrid extends HTMLPanel {

    //--------------------------------------------------------------------------
    //
    //  Fields
    //
    //--------------------------------------------------------------------------

    /**
     * The number of horizontal slices in the grid that divide the total width
     * of the grid.
     */
    protected int totalSlices = 12;

    /**
     * The name of the CSS class used for each row in the grid.
     */
    protected String rowClass = "row-fluid";

    /**
     * Prefix of grid span CSS classes.  Spans are defined by a set of CSS
     * classes like <code>span1</code>, <code>span2</code>, etc., one for each
     * span size.  Each class must following the naming format
     * <code>[PREFIX][N]</code>, where PREFIX is the spanClassPrefix and N is
     * the number of horizontal slices that class equates to.
     */
    protected String spanClassPrefix = "span";

    /**
     * The root element in the widget.
     */
    protected Element root;

    /**
     * If true, when a column has all its children removed, a placeholder will
     * be added that will maintain the width of the column.
     */
    protected boolean preventColumnCollapse = true;

    //--------------------------------------------------------------------------
    //
    //  Constructors
    //
    //--------------------------------------------------------------------------

    /**
     * Constructor
     */
    public FluidGrid() {
        super("");
        
        this.root = this.getElement();
    }

    //--------------------------------------------------------------------------
    //
    //  Properties
    //
    //--------------------------------------------------------------------------

    /**
     * Gets the total number of slices in the grid.  Each slice's width is
     * equal to the total width of this divided by the total number of slices.
     *
     * @return the total number of slices.
     */
    public int getTotalSlices() {
        return totalSlices;
    }

    /**
     * Sets the total number of slices in the grid.  Each slice's width is
     * equal to the total width of this divided by the total number of slices.
     *
     * @param totalSlices The number of slices in the grid.
     */
    public void setTotalSlices(int totalSlices) {
        this.totalSlices = totalSlices;
    }

    /**
     * Gets name of CSS class applied to each row in the grid.
     *
     * @return The name of the CSS class for grid rows.
     */
    public String getRowClass() {
        return rowClass;
    }

    /**
     * Sets name of CSS class applied to each row in the grid.
     *
     * @param rowClass The name of the CSS class for grid rows.
     */
    public void setRowClass(String rowClass) {
        this.rowClass = rowClass;
    }

    /**
     * Gets the CSS class prefix for CSS classes representing different
     * slice spans.
     *
     * @return The span class prefix.
     */
    public String getSpanClassPrefix() {
        return spanClassPrefix;
    }

    /**
     * Sets the CSS class prefix for CSS classes representing different
     * slice spans.
     *
     * @param spanClassPrefix The span class prefix.
     */
    public void setSpanClassPrefix(String spanClassPrefix) {
        this.spanClassPrefix = spanClassPrefix;
    }

    //--------------------------------------------------------------------------
    //
    //  Methods
    //
    //--------------------------------------------------------------------------

    /**
     * Adds new row to the grid.  Rows contain 0 or more columns.
     *
     * @return The index of the row.
     */
    public int addRow() {
        Element rowElement = DOM.createDiv();
        rowElement.setClassName(this.rowClass);
        this.root.appendChild(rowElement);
        return this.root.getChildCount() - 1;
    }

    /**
     * Adds a new column to the specified row with the specified dimensions.
     *
     * @param rowIndex The index of the row to add the column to.
     * @param span Value equal to the number of horizontal slices the new
     *             column should occupy.  If less than 1, it is assumed
     *             that the column should be the max width.  Otherwise,
     *             value should be less than or equal to {@link #totalSlices}.
     *
     * @return The index of the column in the parent row.
     */
    public int addColumn(int rowIndex, int span) {
        if(span > this.totalSlices) {
            throw new IllegalArgumentException(
                    StringUtil.substitute("The specified span must be " +
                    "less than {0}.", span));
        }
        
        if(span < 1) {
            span  = this.totalSlices;
        }

        Element columnElement = DOM.createDiv();
        columnElement.setClassName(this.spanClassPrefix + span);
        Element rowElement = $(this.root).children().eq(rowIndex)
                .append(columnElement).get(0);
        return rowElement.getChildCount() - 1;
    }

    /**
     * Adds a {@link IsWidget} to the specified row and column.
     *
     * @param rowIndex The index of the row to add the child to.
     * @param columnIndex The index of the column to add the child to.
     * @param child The {@link Widget} to add.
     */
    public void add(int rowIndex, int columnIndex, IsWidget child) {
        this.add(rowIndex, columnIndex, child.asWidget());
    }

    /**
     * Adds a {@link Widget} to the specified row and column.
     *
     * @param rowIndex The index of the row to add the child to.
     * @param columnIndex The index of the column to add the child to.
     * @param child The {@link Widget} to add.
     */
    public void add(int rowIndex, int columnIndex, Widget child) {
        Element columnElement = $(this.root).children().eq(rowIndex)
                .children().get(columnIndex);
        this.add(child, columnElement);
    }

    /**
     * Adds a DOM {@link Element} to the specified row and column.
     *
     * @param rowIndex The index of the row to add the child to.
     * @param columnIndex The index of the column to add the child to.
     * @param child The {@link Element} to add.
     */
    public void add(int rowIndex, int columnIndex, Element child) {
        $(this.root).children().eq(rowIndex).children().eq(columnIndex)
                .append(child);
    }

    /**
     * Inserts a widget at the specified coordinates.
     *
     * @param rowIndex The index of the row to insert into.
     * @param columnIndex The index of the column to insert into.
     * @param index The index inside the column to insert at.
     * @param widget The Widget to insert.
     */
    public void insert(int rowIndex, int columnIndex, int index, 
                       IsWidget widget) {
        widget.asWidget().removeFromParent();

        this.getChildren().add(widget.asWidget());
        
        this.insert(rowIndex, columnIndex, index, 
                widget.asWidget().getElement());
        
        this.adopt(widget.asWidget());
    }

    /**
     * Inserts an element at the specified coordinates.
     *
     * @param rowIndex The index of the row to insert into.
     * @param columnIndex The index of the column to insert into.
     * @param index The index inside the column to insert at.
     * @param element The element to insert.
     */
    public void insert(int rowIndex, int columnIndex, int index,
                       Element element) {
        Element column = $(this.root).children().eq(rowIndex).children()
                .get(columnIndex);

        if(index < column.getChildCount()) {
            Node atIndex = column.getChild(index);
            column.insertBefore(element, atIndex);
        }
        else {
            column.appendChild(element);
        }
    }

    /**
     * Gets the coordinates of the specified {@link Widget} in the grid.
     *
     * @param child The widget to retrieve coordinates for.
     *
     * @return Array containing 3 values, rowIndex, columnIndex, index,
     * respectively.
     */
    public int[] getChildCoordinates(Widget child) {
        return this.getChildCoordinates(child.getElement());
    }

    /**
     * Gets the coordinates of the specified {@link Widget} in the grid.
     *
     * @param child The widget to retrieve coordinates for.
     *
     * @return Array containing 3 values, rowIndex, columnIndex, index,
     * respectively.
     */
    public int[] getChildCoordinates(IsWidget child) {
        return this.getChildCoordinates(child.asWidget());
    }

    /**
     * Gets the coordinates of the specified {@link Element} in the grid.
     *
     * @param child The element to retrieve coordinates for.
     *
     * @return Array containing 3 values, rowIndex, columnIndex, index,
     * respectively.
     */
    public int[] getChildCoordinates(Element child) {
        for(int i = 0, lr = this.getElement().getChildCount(); i < lr; i++) {
            Node row = this.getElement().getChild(i);
            for(int j = 0, lc = row.getChildCount(); j < lc; j++) {
                Node column = row.getChild(j);
                for(int k = 0, lk = column.getChildCount(); k < lk; k++) {
                    if(column.getChild(k).equals(child)) {
                        return new int[] {i, j, k};
                    }
                }
            }
        }

        return new int[] {-1, -1, -1};
    }

    /**
     * Gets number of children in a column.
     *
     * @param rowIndex The row's index.
     * @param columnIndex The index of the column.
     *
     * @return The number of children in the column.
     */
    public int getColumnChildCount(int rowIndex, int columnIndex) {
        return this.root.getChild(rowIndex).getChild(columnIndex)
                .getChildCount();
    }

    /**
     * Gets the widget at the specified coordinates.
     *
     * @param rowIndex The index of the child's parent row.
     * @param columnIndex The index of the child's parent column.
     * @param index The index of the child in the column.
     *
     * @return The widget, or null if not found at coordinates.
     */
    public IsWidget getChildAt(int rowIndex, int columnIndex, int index) {
        Element e = $(this.root).children().eq(rowIndex).children()
                .eq(columnIndex).children().get(index);
        for(Widget w : this.getChildren()) {
            if(w.getElement().equals(e)) {
                return w;
            }
        }

        return null;
    }

    @Override
    public void clear() {
        boolean oldPreventColumnCollapse = this.preventColumnCollapse;
        this.preventColumnCollapse = false;
        super.clear();
        this.preventColumnCollapse = oldPreventColumnCollapse;

        //Have to do this extra step because the clear method only removes
        //widgets, not regular elements added additionally.
        $(this.root).empty();
    }

    @Override
    public boolean remove(Widget w) {
        Element parent = w.getElement().getParentElement();
        if(this.preventColumnCollapse && parent.getChildCount() == 1) {
            //Need to insert a placeholder into column in order to not have
            //it collapse its width when the last item is removed.
            $(parent).append("<div>&nbsp;</div>");
        }
        
        return super.remove(w);
    }
}
